package game.dungeon;

import game.dungeon.position.Direction;
import game.dungeon.position.Position;
import game.square.Rock;
import game.square.Square;
import game.square.SquareImpl;
import game.square.temperature.TemperatureScale;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * A class of dungeons.
 * 
 * @invar 	... 
 * 		  | hasProperSquares()
 * 
 * @version 1.0
 * @author Chris Desomer, Geert Heyman
 */
public abstract class Dungeon<S extends SquareImpl> {
	
	/**
	 * Initialize a new dungeon.
	 *
	 * @param 	boundaryPosition
	 * 			The boundary position for this dungeon.
	 * @pre		The given boundary position must be effective.
	 * 		  | boundaryPosition != null
	 * @post	The boundary position of this dungeon is equal to
	 * 			the given boundary position.
	 * 		  |	new.getBoundaryPosition() == boundaryPosition
	 * @post	..
	 * 		  | new.getSquares() != null
	 */
	@Raw
	public Dungeon(Position boundaryPosition){
		assert (boundaryPosition != null);
		this.boundaryPosition = boundaryPosition;
	}
	
	/**
	 * Get the boundary position of this dungeon.
	 */
	@Basic
	@Immutable
	public Position getBoundaryPosition(){
		return boundaryPosition;
	}
	
	/**
	 * Variable that references the position with the maximum value for 
	 * each coordinate in this dungeon.
	 */
	private final Position boundaryPosition;
	
	/**
	 * Check if this dungeon is terminated or not.
	 */
	@Basic
	public boolean isTerminated(){
		return terminated;
	}
	
	/**
	 * Terminate this dungeon.
	 * 
	 * @post	This dungeon is terminated
	 * 		  | new.isTerminated()
	 */
	public void terminate(){
		for (Square square : getSquares().values())
			square.terminate();
		getSquares().clear();
		terminated = true;
	}
	
	/**
	 * Variable that stores if this dungeon is terminated or not.
	 */
	private boolean terminated;
	
	/**
	 * Check if the positions of this dungeon overlap with the positions of 
	 * the direct sub dungeons of the given composite dungeon.
	 * 
	 * @param 	compositeDungeon
	 * 			The composite dungeon that contains the sub dungeons, to check
	 * 			overlap with.
	 * @param 	originInRefToCompositeDungeon
	 * 			The position of the origin of this dungeon in reference to the
	 * 			origin of the given composite dungeon.
	 * @return	False, if and only if at least one of the positions of this 
	 * 			dungeon, overlaps with one of the positions in the sub dungeons
	 * 			of the given composite dungeon.
	 * @note 	The given composite dungeon has a generic type that extends SquareImpl,
	 * 			not S!
	 */
	public boolean isDisjunctWithDungeonsOfCompositeDungeon(
			CompositeDungeon<? extends SquareImpl> compositeDungeon,
			Position originInRefToCompositeDungeon) {
		Map<Position,? extends Dungeon<? extends SquareImpl>> directSubDungeons = compositeDungeon.getDirectSubDungeons();
		for (Position originOther : directSubDungeons.keySet()){
			Dungeon<? extends SquareImpl> subDungeon = compositeDungeon.getDirectSubDungeons().get(originOther); 
			Position boundaryPositionSubDungeon = subDungeon.getBoundaryPosition().add(originOther);
			if (subDungeon != this)	// check if the origin position doesn't refer to this dungeon.
				if (!Position.areDisjunctCubes(originOther,
						boundaryPositionSubDungeon, originInRefToCompositeDungeon,
						getBoundaryPosition()
								.add(originInRefToCompositeDungeon)))
					return false;
		}
		return true;
	}
	
	/**
	 * Get a copy of the map of squares of this dungeon.  
	 */
	@Basic
	public abstract Map<Position, S> getSquares();
	
	/**
	 * Get the square at the given position.
	 * 
	 * @param 	position
	 * 			The position to get the square at.
	 * @pre		This dungeon must have a square at the 
	 * 			given position.
	 * @return	..
	 * 		  | result == getSquares().get(position)
	 */
	public S getSquareAtPosition(Position position){
		assert (getSquares().get(position) != null);
		return getSquares().get(position);
	}
	
	/**
	 * Check if this dungeon can have the given position as one 
	 * of its positions.
	 * 
	 * @param 	position
	 * 			The position to check.
	 * @return	..
	 * 		  | if (!containsPosition(position))
	 * 		  |		then result == false
	 * @result	..
	 * 		  |	if (position.getXCoordinate() == position.getYCoordinate()
	 *		  |			&& position.getYCoordinate() == position.getZCoordinate())
	 *		  |		then result == false	
	 * @result	..
	 * 		  | else result == true
	 */
	@Raw
	public boolean canHaveAsPosition(Position position){
		if (!containsPosition(position))
			return false;
		if (position.getXCoordinate() == position.getYCoordinate()
				&& position.getYCoordinate() == position.getZCoordinate())
			return false;
		return true;	
	}
	
	/**
	 * Check if this dungeon can have the given square as one of its squares
	 * at the given position.
	 * @param 	square
	 * 			The square to check.
	 * @param	position
	 * 			The position to check the square at.
	 * @return ..
	 * 		  | if(square == null || position == null)
	 * 		  | 	then result == false
	 * @result	..
	 * 		  |	if (square.isTerminated() || isTerminated())
	 * 		  |		then result == false
	 * @result	..
	 * 		  |	if (!canHaveAsPosition())
	 *		  |		then result == false	
	 * @result 	Otherwise the result is true.
	 *		  |	else result == true  
	 */
	@Raw
	public boolean canHaveAsSquareAtPosition(S square, Position position){
		if(square == null || position == null)
			return false;
		if (square.isTerminated() || isTerminated())
			return false;
		if (!canHaveAsPosition(position))
			return false;
		return true;
	}
	
	/**
	 * Check if this dungeon has proper squares.
	 * 
	 * @return	If this dungeon is terminated, the result is true.
	 * @result	If this dungeon has no squares, the result is true.
	 * @result	If the percentage of slippery squares in this dungeon is higher than 20 percent,
	 * 			the result is false.
	 * @result	Otherwise, if for at least one square of this dungeon the number of dungeons
	 * 			it belongs to isn't 1, then the result is false.
	 * @result	Otherwise,if for at least one square of this dungeon, this dungeon can't
	 * 			have the given square at its position, then the result is false.
	 * @result	Otherwise,if for at least one square of this dungeon, the square doesn't share
	 * 			the same border with squares located at a position next to it.
	 * @result	Otherwise, the result is true.
	 */
	@Raw
	public boolean hasProperSquares(){
		if (isTerminated())
			return true;
		if (getSquares().isEmpty())
			return true;
		if (getSlipperySquarePercentage(0) > 20)
			return false;
		for (Position position: getSquares().keySet()){
			if (getSquares().get(position).getNbOfDungeons() != 1)
				return false;
			if (!canHaveAsSquareAtPosition(getSquares().get(position),position))
				return false;
			if (!neighborsShareBorder(position))
				return false;
		}
		return true;
	}
	
	/*
	 * Private method to check weather the square at the given position, shares the same
	 * borders with his neighbors.
	 */
	private boolean neighborsShareBorder(Position position) {
		for (Direction direction : Direction.values()){
			S neighborSquare = getSquares().get(position.getPositionInDirection(direction));
			if (neighborSquare != null
					&& !(getSquares().get(position).getBorderInDirection(
							direction) == neighborSquare
							.getBorderInDirection(direction
									.getOppositeDirection())))
					return false;
		}
		return true;
	}
	
	/*
	 * Private method to calculate the percentage of slippery squares of this
	 * dungeon.
	 * 
	 * @param	nbOfSlipperySquaresToAdd
	 * 			The number of slippery squares that don't belong to this dungeon
	 * 			but that you wish to take in account in the calculation.
	 */
	private double getSlipperySquarePercentage(int nbOfSlipperySquaresToAdd){
		int nbOfSlipperySquares = nbOfSlipperySquaresToAdd;
		for (S square : getSquares().values()){
			if (square.isSlipperyMaterial())
				nbOfSlipperySquares++;
		}
		double percentage = nbOfSlipperySquares/(getSquares().size()+nbOfSlipperySquaresToAdd);
		return percentage * 100;
	}
	
	/**
	 * Add a square to this dungeon at the given position.
	 * 
	 * @param 	square
	 * 			The square to add.
	 * @param 	position
	 * 			The position where the square should be added.
	 * @pre 	...
	 * 		  | canHaveAsSquareAtPosition(square,position)
	 * @pre		...	
	 * 		  |	square.getNbOfDungeons() == 0
	 * @pre		The given square isn't slippery or the percentage of slippery squares
	 * 			if the given square is added isn't higher than 20%
	 *
	 */
	public void addSquareAtPosition(S square,Position position) {
		assert(canHaveAsSquareAtPosition(square,position));
		assert(square.getNbOfDungeons() == 0);
		assert(!square.isSlipperyMaterial() || getSlipperySquarePercentage(1) <= 20);
	}
	
	/**
	 * Remove the given square from the squares of this dungeons.
	 * 
	 * @param 	square
	 * 			The square to remove.
	 * @pre		..
	 * 		  | square != null
	 * @pre		..
	 * 		  |	getSquares().containsValue(square)
	 */
	public void removeSquare(SquareImpl square){
		assert (square != null);
		assert (getSquares().containsValue(square));
	}
	
	/**
	 * Get the dungeon that contains this dungeon.
	 */
	@Basic
	@Immutable
	public Dungeon<? super S> getEnclosingDungeon(){
		return enclosingDungeon;
	}
	
	/**
	 * Set the given enclosing dungeon as the enclosing dungeon of this dungeon.
	 * 
	 * @param	enclosingDungeon
	 * 			The new enclosing dungeon for this dungeon
	 * @pre		..
	 * 		  | canHaveAsEnclosingDungeon(enclosingDungeon)
	 * @pre		..
	 * 		  | getEnclosingDungeon() == null
	 * @post	..
	 * 		  | new.getEnclosingDungeon() == enclosingDungeon
	 * @note	The enclosing dungeon must be a super type of S, otherwise not
	 * 			all squares contained by the enclosing dungeon are from the same
	 * 			type.
	 */
	public void setEnclosingDungeon(CompositeDungeon<? super S> enclosingDungeon){
		assert (canHaveAsEnclosingDungeon(enclosingDungeon));
		assert (this.enclosingDungeon == null);
		this.enclosingDungeon = enclosingDungeon;
	}
	
	/**
	 * Check if this dungeon can have the given composite dungeon as its 
	 * enclosing dungeon.
	 *  
	 * @param 	dungeon
	 * 			The dungeon to check.
	 * @return	..
	 * 		   | result == dungeon.getDirectSubDungeons().containsValue(this)
	 */
	public boolean canHaveAsEnclosingDungeon(CompositeDungeon<? super S> dungeon) {
		return dungeon.getDirectSubDungeons().containsValue(this);
	}

	/**
	 * Variable that references the enclosing dungeon of this dungeon.
	 */
	private Dungeon<? super S> enclosingDungeon;
	
	/**
	 * Get a set of the singular dungeons enclosed by this composite dungeon.
	 */
	public abstract Set<SingularDungeon<? extends S>> getSingularDungeons();
	
	/**
	 * Check if this dungeon contains the given position.
	 * 
	 * @param	position
	 * 			The position to check. 		    
	 *
	 */
	public abstract boolean containsPosition(Position position);
	
	/**
	 * Get the root dungeon of this dungeon.
	 * 
	 * @return	..
	 * 		  | if (getEnlosingDungeon() == null)
	 * 		  |		result == this
	 * @result	..
	 * 		  | else result == getEnclosingDungeon().getRootDungeon()
	 */
	public Dungeon<? super S> getRootDungeon(){
		if (getEnclosingDungeon() == null)
			return this;
		return getEnclosingDungeon().getRootDungeon();
	}
	
	/**
	 * Return an iterator returning all the elements in this 
	 * dungeon.
	 * 
	 * @return	Each element in the result is a square of this dungeon.
	 */
	 public Iterator<S> iterator() {
	        return new Iterator<S>() {

	            public boolean hasNext() {
	            	if (currentIterator == null)
	            		if (singularDungeonsIterator.hasNext())
	            			currentIterator = singularDungeonsIterator.next().iterator();
	            		else {
	            			return false;
	            		}
	                return currentIterator.hasNext() || singularDungeonsIterator.hasNext();
	            }

	            public S next() throws NoSuchElementException {
	                if (!hasNext())
	                    throw new NoSuchElementException();
	                if (currentIterator.hasNext())
	                	return currentIterator.next();
	                currentIterator = singularDungeonsIterator.next().iterator();
	                return currentIterator.next();
	            }

	            public void remove() throws UnsupportedOperationException {
	                throw new UnsupportedOperationException();
	            }
	            private Iterator<SingularDungeon<? extends S>> singularDungeonsIterator = getSingularDungeons().iterator();
	            private Iterator<? extends S> currentIterator; 

	        };
	    }
	 
	 /**
	  * Interface for the predicate used in the filter method, to filter certain elements
	  * from an iterator.
	  */
	 public interface Predicate<T> { boolean apply(T type); }

	 /**
	  * Method to filter an iterator with elements of type S, on the conditions defined
	  * by the given predicate.
	  * 
	  * @param	unfilteredSquares
	  * 		The iterator with elements of type S, that needs to be filtered.
	  * @param 	predicate
	  * 		The predicate that defines the conditions on which the given squares
	  * 		iterator needs to be filtered.
	  * @return	The result is a list of squares of type S that satisfy the conditions
	  * 		defined by the given predicate.		
	  */
	 public List<S> filter(Iterator<S> unfilteredSquares, Predicate<S> predicate) {
		    List<S> filteredSquares = new ArrayList<S>();
		    while (unfilteredSquares.hasNext()){
		    	S square = unfilteredSquares.next();
		        if (predicate.apply(square)) {
		            filteredSquares.add(square);
		        }
		    }
		    return filteredSquares;
		}
	 
	/**
	 * Return all rocks of this dungeon with a temperature greater than or equal as 200 degrees Celcius
	 * 
	 * @return 	..
	 * 		  | {rocks : getSquares() | rock instanceof Rock && rock.getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) >= 200}  
	 */
	 public List<S> getHotRocksCelcius(){
		 Predicate<S> predicate = new Predicate<S>() {
			    public boolean apply(S square) {
			    	return square instanceof Rock && square.getTemperature().getTemperatureValue(
								TemperatureScale.CELCIUS) >= 200;
			    }
			};
		return filter(iterator(), predicate);	
	 }
	
}
